GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Push — master ( 57766b...766277 )
by Richard
02:44
created

ticketer.go   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 3
rs 10
c 1
b 0
f 0
cc 1
nc 1
nop 0
1
/**
2
 * Created by wechsler on 04/08/15.
3
 */
4
var ticketer = (function() {
5
  'use strict';
6
7
  return {
8
    upcomingTicketTemplate: null,
9
    manageTemplate: null,
10
    songAutocompleteItemTemplate: null,
11
    editTicketTemplate: null,
12
    songDetailsTemplate: null,
13
    appMessageTarget: null,
14
    searchCount: 10,
15
    instrumentOrder: ['V', 'G', 'B', 'D', 'K'],
16
    defaultSongLengthSeconds: 240,
17
    defaultSongIntervalSeconds: 120,
18
    messageTimer: null,
19
20
    /**
21
     * @var {{songInPreview,upcomingCount,iconMapHtml}}
22
     */
23
    displayOptions: {},
24
25
    /**
26
     * List of all performers (objects) who've signed up in this session
27
     */
28
    performers: [],
29
30
    /**
31
     * List of all platform names in the system
32
     */
33
    platforms: [],
34
35
    performerExists: function(performerName) {
36
      for (var i = 0; i < this.performers.length; i++) {
37
        if (this.performers[i].performerName.toLowerCase() == performerName.toLowerCase()) {
38
          return true;
39
        }
40
      }
41
      return false;
42
    },
43
44
    addPerformerByName: function(performerName) {
45
      this.performers.push({performerName: performerName});
46
      // Now resort it
47
      this.performers.sort(function(a, b) {
48
        return a.performerName.localeCompare(b.performerName);
49
      });
50
    },
51
52
    /**
53
     * Run the "upcoming" panel
54
     */
55
    go: function() {
56
      this.initTemplates();
57
58
      ticketer.reloadTickets();
59
      setInterval(function() {
60
        ticketer.reloadTickets();
61
      }, 10000);
62
    },
63
64
    /**
65
     * Draw an "upcoming" ticket
66
     * @param ticket {{band}}
67
     * @returns {*}
68
     */
69
    drawDisplayTicket: function(ticket) {
70
      // Sort band into standard order
71
      var unsortedBand = ticket.band;
72
      var sortedBand = {};
73
      for (var i = 0; i < this.instrumentOrder.length; i++) {
74
        var instrument = this.instrumentOrder[i];
75
        if (unsortedBand.hasOwnProperty(instrument)) {
76
          sortedBand[instrument] = unsortedBand[instrument];
77
        }
78
      }
79
      ticket.band = sortedBand;
80
      var ticketParams = {ticket: ticket, icons: this.displayOptions.iconMapHtml};
81
      return this.upcomingTicketTemplate(ticketParams);
82
    },
83
84
    /**
85
     * Draw a "queue management" ticket
86
     * @param ticket
87
     * @returns {*}
88
     */
89
    drawManageableTicket: function(ticket) {
90
      ticket.used = Number(ticket.used); // Force int
91
92
      return this.manageTemplate({ticket: ticket});
93
    },
94
95
    /**
96
     * Reload all tickets on the upcoming page
97
     */
98
    reloadTickets: function() {
99
      var that = this;
100
101
      $.get('/api/next', function(tickets) {
102
103
        var out = '';
104
        for (var i = 0; i < tickets.length; i++) {
105
          var ticket = tickets[i];
106
          out += that.drawDisplayTicket(ticket);
107
        }
108
109
        var target = $('#target');
110
        target.html(out);
111
112
        target.find('.auto-font').each(
113
          function() {
114
            var fixedWidth = $(this).data('fixed-assetwidth');
115
            if (!fixedWidth) {
116
              fixedWidth = 0;
117
            }
118
            fixedWidth = Number(fixedWidth);
119
120
            var spaceUsedByText = (this.scrollWidth - fixedWidth);
121
            var spaceAvailableForText = (this.clientWidth - fixedWidth);
122
            var rawScale = Math.max(spaceUsedByText / spaceAvailableForText, 1);
123
            var scale = 1.05 * rawScale;
124
125
            if (that.displayOptions.adminQueueHasControls && that.displayOptions.isAdmin) {
126
              scale *= 1.25;
127
            }
128
129
            // 1.05 extra scale to fit neatly, fixedWidth is non-scaling elements
130
            var font = Number($(this).css('font-size').replace(/[^0-9]+$/, ''));
131
            $(this).css('font-size', Number(font / scale).toFixed() + 'px');
132
          }
133
        );
134
135
        target.find('.performingButton').click(function() {
136
          var ticketId = $(this).data('ticket-id');
0 ignored issues
show
Unused Code introduced by
The variable ticketId seems to be never used. Consider removing it.
Loading history...
137
          if (window.confirm('Mark song as performing?')) {
138
            that.performButtonCallback(this);
139
          }
140
        });
141
        target.find('.removeButton').click(function() {
142
          var ticketId = $(this).data('ticket-id');
0 ignored issues
show
Unused Code introduced by
The variable ticketId seems to be never used. Consider removing it.
Loading history...
143
          if (window.confirm('Remove song?')) {
144
            that.removeButtonCallback(this);
145
          }
146
        });
147
148
      });
149
    },
150
151
    /**
152
     * Enable queue management ticket buttons in the specified element
153
     * @param topElement
154
     */
155
    enableButtons: function(topElement) {
156
      var that = this;
157
158
      $(topElement).find('.performButton').click(function() {
159
        that.performButtonCallback(this);
160
      });
161
162
      $(topElement).find('.removeButton').click(function() {
163
        that.removeButtonCallback(this);
164
      });
165
166
      $(topElement).find('.editButton').click(function() {
167
        that.editButtonCallback(this);
168
      });
169
    },
170
171
    enableSongSearchBox: function(songSearchInput, songSearchResultsTarget, songClickHandler) {
172
      var that = this;
173
      $(songSearchInput).keyup(
174
        function() {
175
          var songComplete = $(songSearchResultsTarget);
176
          var input = $(this);
177
          var searchString = input.val();
178
          if (searchString.length >= 3) {
179
            $.ajax({
180
              method: 'POST',
181
              data: {
182
                searchString: searchString,
183
                searchCount: that.searchCount
184
              },
185
              url: '/api/songSearch',
186
              /**
187
               * @param {{songs, searchString}} data
188
               */
189
              success: function(data) {
190
                var songs = data.songs;
191
                if (input.val() == data.searchString) {
192
                  // Ensure autocomplete response is still valid for current input value
193
                  var out = '';
194
                  var song; // Used twice below
195
                  for (var i = 0; i < songs.length; i++) {
196
                    song = songs[i];
197
                    out += that.songAutocompleteItemTemplate({song: song});
198
                  }
199
                  songComplete.html(out).show();
200
201
                  // Now attach whole song as data:
202
                  for (i = 0; i < songs.length; i++) {
203
                    song = songs[i];
204
                    var songId = song.id;
205
                    songComplete.find('.acSong[data-song-id=' + songId + ']').data('song', song);
206
                  }
207
208
                  that.enableAcSongSelector(songComplete, songClickHandler);
209
                }
210
              },
211
              error: function(xhr, status, error) {
212
                void(error);
213
              }
214
            });
215
          } else {
216
            songComplete.html('');
217
          }
218
        }
219
      );
220
    },
221
222
    /**
223
     * Completely (re)generate the add ticket control panel and enable its controls
224
     * @param {?number} currentTicket Optional
225
     */
226
    resetEditTicketBlock: function(currentTicket) {
227
      var that = this;
228
      var controlPanelOuter = $('.editTicketOuter');
229
230
      // Current panel state in function scope
231
      var selectedInstrument = 'V';
232
      var currentBand = {};
233
234
      // Reset band to empty (or to ticket band state)
235
      for (var instrumentIdx = 0; instrumentIdx < that.instrumentOrder.length; instrumentIdx++) {
236
        var instrument = that.instrumentOrder[instrumentIdx];
237
        currentBand[instrument] = [];
238
239
        if (currentTicket && currentTicket.band) {
240
          // Ticket.band is a complex datatype. Current band is just one array of names per instrument. Unpack to show.
241
          if (currentTicket.band.hasOwnProperty(instrument)) {
242
            var instrumentPerformerObjects = currentTicket.band[instrument];
243
            for (var pIdx = 0; pIdx < instrumentPerformerObjects.length; pIdx++) {
244
              currentBand[instrument].push(instrumentPerformerObjects[pIdx].performerName);
245
            }
246
          }
247
        }
248
        // Store all instruments as arrays - most can only be single, but vocals is 1..n potentially
249
      }
250
251
      drawEditTicketForm(currentTicket);
252
      // X var editTicketBlock = $('.editTicket'); // only used in inner scope (applyNewSong)
253
254
      // Enable 'Add' button
255
      $('.editTicketButton').click(editTicketCallback);
256
      $('.cancelTicketButton').click(cancelTicketCallback);
257
      $('.removeSongButton').click(removeSong);
258
259
      $('.toggleButton').click(
260
        function() {
261
          var check = $(this).find('input[type=checkbox]');
262
          check.prop('checked', !check.prop('checked'));
263
        }
264
      );
265
266
      // Enable the instrument tabs
267
      var allInstrumentTabs = controlPanelOuter.find('.instrument');
268
269
      allInstrumentTabs.click(
270
        function() {
271
          selectedInstrument = $(this).data('instrumentShortcode');
272
          setActiveTab(selectedInstrument);
273
        }
274
      );
275
276
      var ticketTitleInput = $('.editTicketTitle');
277
278
      // Copy band name into summary area on Enter
279
      ticketTitleInput.keydown(function(e) {
280
        if (e.keyCode == 13) {
281
          updateBandSummary();
282
        }
283
      });
284
285
      $('.newPerformer').keydown(function(e) {
286
        if (e.keyCode == 13) {
287
          var newPerformerInput = $('.newPerformer');
288
          var newName = newPerformerInput.val();
289
          if (newName.trim().length) {
290
            alterInstrumentPerformerList(selectedInstrument, newName, true);
291
          }
292
          newPerformerInput.val('');
293
        }
294
      });
295
296
      // Set up the song search box in this control panel and set the appropriate callback
297
      var songSearchInput = '.addSongTitle';
298
      var songSearchResultsTarget = '.songComplete';
299
300
      this.enableSongSearchBox(songSearchInput, songSearchResultsTarget, applyNewSong);
301
302
      // ************* Inner functions **************
303
      /**
304
       * Switch to the next visible instrument tab
305
       */
306
      function nextInstrumentTab() {
307
        // Find what offset we're at in instrumentOrder
308
        var currentOffset = 0;
309
        for (var i = 0; i < that.instrumentOrder.length; i++) {
310
          if (that.instrumentOrder[i] == selectedInstrument) {
311
            currentOffset = i;
312
          }
313
        }
314
        var nextOffset = currentOffset + 1;
315
        if (nextOffset >= that.instrumentOrder.length) {
316
          nextOffset = 0;
317
        }
318
        var instrument = that.instrumentOrder[nextOffset];
319
        selectedInstrument = instrument; // Reset before we redraw tabs
320
        var newActiveTab = setActiveTab(instrument);
321
322
        // Make sure we switch to a *visible* tab
323
        if (newActiveTab.hasClass('instrumentUnused')) {
324
          nextInstrumentTab();
325
        }
326
      }
327
328
      /**
329
       * (re)Draw the add/edit ticket control panel in the .editTicketOuter element
330
       */
331
      function drawEditTicketForm(ticket) {
332
        var templateParams = {performers: that.performers};
333
        if (ticket) {
334
          templateParams.ticket = ticket;
335
        }
336
        controlPanelOuter.html(that.editTicketTemplate(templateParams));
337
        updateInstrumentTabs();
338
        rebuildPerformerList(controlPanelOuter.find('.performers'));
0 ignored issues
show
Bug introduced by
The call to rebuildPerformerList seems to have too many arguments starting with controlPanelOuter.find(".performers").
Loading history...
339
        if (ticket && ticket.song) {
340
          applyNewSong(ticket.song);
341
        }
342
      }
343
344
      function findPerformerInstrument(name) {
345
        var instrumentPlayers;
346
        for (var instrumentCode in currentBand) {
347
          if (currentBand.hasOwnProperty(instrumentCode)) {
348
            instrumentPlayers = currentBand[instrumentCode];
349
            for (var i = 0; i < instrumentPlayers.length; i++) {
350
              if (instrumentPlayers[i].toUpperCase() == name.toUpperCase()) {
351
                return instrumentCode;
352
              }
353
            }
354
          }
355
        }
356
        return null;
357
      }
358
359
      /**
360
       * Rebuild list of performer buttons according to overall performers list
361
       * and which instruments they are assigned to
362
       */
363
      function rebuildPerformerList() {
364
        var newButton;
365
        var targetElement = controlPanelOuter.find('.performers');
366
        targetElement.text(''); // Remove existing list
367
368
        var lastInitial = '';
369
        var performerCount = that.performers.length;
370
        var letterSpan;
371
        for (var pIdx = 0; pIdx < performerCount; pIdx++) {
372
          var performerName = that.performers[pIdx].performerName;
373
          var performerInstrument = findPerformerInstrument(performerName);
374
          var isPerforming = performerInstrument ? 1 : 0;
375
          var initialLetter = performerName.charAt(0).toUpperCase();
376
          if (lastInitial !== initialLetter) {
377
            if (letterSpan) {
378
              targetElement.append(letterSpan);
379
            }
380
            letterSpan = $('<span class="letterSpan"></span>');
381
            if ((performerCount > 15)) {
382
              letterSpan.append($('<span class="initialLetter">' + initialLetter + '</span>'));
383
            }
384
          }
385
          lastInitial = initialLetter;
386
387
          newButton = $('<span></span>');
388
          newButton.addClass('btn addPerformerButton');
389
          newButton.addClass(isPerforming ? 'btn-primary' : 'btn-default');
390
          if (isPerforming && (performerInstrument !== selectedInstrument)) { // Dim out buttons for other instruments
391
            newButton.attr('disabled', 'disabled');
392
          }
393
          newButton.text(performerName);
394
          newButton.data('selected', isPerforming); // This is where it gets fun - check if user is in band!
395
          letterSpan.append(newButton);
0 ignored issues
show
Bug introduced by
The variable letterSpan seems to not be initialized for all possible execution paths.
Loading history...
396
        }
397
        targetElement.append(letterSpan);
398
399
        // Enable the new buttons
400
        $('.addPerformerButton').click(function() {
401
          var name = $(this).text();
402
          var selected = $(this).data('selected') ? 0 : 1; // Reverse to get new state
403
          if (selected) {
404
            $(this).removeClass('btn-default').addClass('btn-primary');
405
          } else {
406
            $(this).removeClass('btn-primary').addClass('btn-default');
407
          }
408
          $(this).data('selected', selected); // Toggle
409
410
          alterInstrumentPerformerList(selectedInstrument, name, selected);
411
        });
412
      }
413
414
      /**
415
       * Handle click on edit ticket button
416
       */
417
      function editTicketCallback() {
418
        var titleInput = $('.editTicketTitle');
419
        var ticketTitle = titleInput.val();
420
        var songInput = $('.selectedSongId');
421
        var songId = songInput.val();
422
        var privateCheckbox = $('input.privateCheckbox');
423
        var isPrivate = privateCheckbox.is(':checked');
424
        var blockingCheckbox = $('input.blockingCheckbox');
425
        var isBlocked = blockingCheckbox.is(':checked');
426
427
        var data = {
428
          title: ticketTitle,
429
          songId: songId,
430
          band: currentBand,
431
          private: isPrivate,
432
          blocking: isBlocked
433
        };
434
435
        if (currentTicket) {
436
          data.existingTicketId = currentTicket.id;
437
        }
438
439
        that.showAppMessage('Saving ticket');
440
441
        $.ajax({
442
            method: 'POST',
443
            data: data,
444
            url: '/api/saveTicket',
445
            success: function(data, status) {
446
              that.showAppMessage('Saved ticket', 'success');
447
448
              void(status);
449
              var ticketId = data.ticket.id;
450
451
              var ticketBlockSelector = '.ticket[data-ticket-id="' + ticketId + '"]';
452
              var existingTicketBlock = $(ticketBlockSelector);
453
              if (existingTicketBlock.length) {
454
                // Replace existing
455
                existingTicketBlock.after(that.drawManageableTicket(data.ticket));
456
                existingTicketBlock.remove();
457
              } else {
458
                // Append new
459
                $('#target').append(that.drawManageableTicket(data.ticket));
460
              }
461
462
              var ticketBlock = $(ticketBlockSelector);
463
              ticketBlock.data('ticket', data.ticket);
464
              that.enableButtons(ticketBlock);
465
466
              if (data.performers) {
467
                that.performers = data.performers;
468
              }
469
470
              that.updatePerformanceStats();
471
              that.resetEditTicketBlock();
472
473
            },
474
            error: function(xhr, status, error) {
475
              var message = 'Ticket save failed';
476
              that.reportAjaxError(message, xhr, status, error);
477
              void(error);
478
              // FIXME handle error
479
            }
480
          }
481
        );
482
      }
483
484
      function cancelTicketCallback() {
485
        that.resetEditTicketBlock();
486
      }
487
488
      function getTabByInstrument(instrument) {
489
        return controlPanelOuter.find('.instrument[data-instrument-shortcode=' + instrument + ']');
490
      }
491
492
      function setActiveTab(selectedInstrument) {
493
        allInstrumentTabs.removeClass('instrumentSelected');
494
        var selectedTab = getTabByInstrument(selectedInstrument);
495
        selectedTab.addClass('instrumentSelected');
496
        rebuildPerformerList(); // Rebuild in current context
497
        return selectedTab;
498
      }
499
500
      function updateBandSummary() {
501
        var bandName = $('.editTicketTitle').val();
502
        var members = [];
503
        for (var instrument in currentBand) {
504
          if (currentBand.hasOwnProperty(instrument)) {
505
            for (var i = 0; i < currentBand[instrument].length; i++) {
506
              members.push(currentBand[instrument][i]);
507
            }
508
          }
509
        }
510
        var memberList = members.join(', ');
511
        var summaryHtml = (bandName ? bandName + '<br />' : '') + memberList;
512
        $('.selectedBand').html(summaryHtml);
513
      }
514
515
      function updateInstrumentTabs() {
516
        var performersSpan;
517
        var performerString;
518
519
        for (var iIdx = 0; iIdx < that.instrumentOrder.length; iIdx++) {
520
          var instrument = that.instrumentOrder[iIdx];
521
522
          performersSpan = controlPanelOuter
523
            .find('.instrument[data-instrument-shortcode=' + instrument + ']')
524
            .find('.instrumentPerformer');
525
526
          performerString = currentBand[instrument].join(', ');
527
          if (!performerString) {
528
            performerString = '<i>Needed</i>';
529
          }
530
          performersSpan.html(performerString);
531
        }
532
533
        updateBandSummary();
534
      }
535
536
      /**
537
       * Handle performer add / remove by performer button / text input
538
       * @param instrument
539
       * @param changedPerformer
540
       * @param isAdd
541
       */
542
      function alterInstrumentPerformerList(instrument, changedPerformer, isAdd) {
543
        var currentInstrumentPerformers = currentBand[selectedInstrument];
544
545
        var newInstrumentPerformers = [];
546
        for (var i = 0; i < currentInstrumentPerformers.length; i++) {
547
          var member = currentInstrumentPerformers[i].trim(); // Trim only required when we draw data from manual input
548
          if (member.length) {
549
            if (member.toUpperCase() != changedPerformer.toUpperCase()) {
550
              // If it's not the name on our button, no change
551
              newInstrumentPerformers.push(member);
552
            }
553
          }
554
        }
555
556
        if (isAdd) { // If we've just selected a new user, append them
557
          newInstrumentPerformers.push(changedPerformer);
558
          if (!that.performerExists(changedPerformer)) {
559
            that.addPerformerByName(changedPerformer);
560
          }
561
        }
562
563
        currentBand[selectedInstrument] = newInstrumentPerformers;
564
        // Now update band with new performers of this instrument
565
566
        updateInstrumentTabs();
567
        rebuildPerformerList();
568
569
        if (newInstrumentPerformers.length) { // If we've a performer for this instrument, skip to next
570
          nextInstrumentTab();
571
        }
572
573
      }
574
575
      /**
576
       *
577
       * @param {{id, title, artist, hasKeys, hasHarmony}} song
578
       */
579
      function applyNewSong(song) {
580
        var selectedId = song.id;
581
        var selectedSong = song.artist + ': ' + song.title;
582
583
        var removeSongButton = $('.removeSongButton');
584
        removeSongButton.removeClass('hidden');
585
586
        // Perform actions with selected song
587
        var editTicketBlock = $('.editTicket'); // Temp hack, should already be in scope?
588
589
        editTicketBlock.find('input.selectedSongId').val(selectedId);
590
        editTicketBlock.find('.selectedSong').text(selectedSong);
591
        var keysTab = controlPanelOuter.find('.instrumentKeys');
592
        if (song.instruments && (song.instruments.indexOf('Keyboard') !== -1)) {
593
          keysTab.removeClass('instrumentUnused');
594
        } else {
595
          keysTab.addClass('instrumentUnused');
596
          // Also uncheck any performer for instrument (allow use elsewhere)
597
          currentBand.K = [];
598
          keysTab.find('.instrumentPerformer').html('<i>Needed</i>');
599
          rebuildPerformerList();
600
        }
601
      }
602
603
      function removeSong() {
604
        var editTicketBlock = $('.editTicket'); // Temp hack, should already be in scope?
605
        editTicketBlock.find('input.selectedSongId').val(0);
606
        editTicketBlock.find('.selectedSong').text('');
607
        $(songSearchInput).val('');
608
        var removeSongButton = $('.removeSongButton');
609
        removeSongButton.hide();
610
611
      }
612
    },
613
614
    manage: function(tickets) {
615
      var that = this;
616
      this.appMessageTarget = $('#appMessages');
617
      this.initTemplates();
618
      var ticket, ticketBlock; // For loop iterations
619
620
      var out = '';
621
      for (var i = 0; i < tickets.length; i++) {
622
        ticket = tickets[i];
623
        out += that.drawManageableTicket(ticket);
624
        ticketBlock = $('.ticket[data-ticket-id="' + ticket.id + '"]');
625
        ticketBlock.data('ticket', ticket);
626
      }
627
      $('#target').html(out);
628
629
      // Find new tickets (now they're DOM'd) and add data to them
630
      for (i = 0; i < tickets.length; i++) {
631
        ticket = tickets[i];
632
        ticketBlock = $('.ticket[data-ticket-id="' + ticket.id + '"]');
633
        ticketBlock.data('ticket', ticket);
634
      }
635
636
      var $sortContainer = $('.sortContainer');
637
      $sortContainer.sortable({
638
        axis: 'y',
639
        update: function(event, ui) {
640
          void(event);
641
          void(ui);
642
          that.ticketOrderChanged();
643
        }
644
      }).disableSelection().css('cursor', 'move');
645
646
      this.enableButtons($sortContainer);
647
648
      this.updatePerformanceStats();
649
650
      this.resetEditTicketBlock();
651
652
    },
653
654
    initSearchPage: function() {
655
      var that = this;
656
      this.initTemplates();
657
      this.enableSongSearchBox('.searchString', '.songComplete', that.searchPageSongSelectionClick);
658
    },
659
660
    initTemplates: function() {
661
      var that = this;
662
663
      // CommaList = each, with commas joining. Returns value at t as tuple {k,v}
664
      // "The options hash contains a function (options.fn) that behaves like a normal compiled Handlebars template."
665
      // If called without inner template, options.fn is not populated
666
      Handlebars.registerHelper('commalist', function(context, options) {
667
        var retList = [];
668
669
        for (var key in context) {
670
          if (context.hasOwnProperty(key)) {
671
            retList.push(options.fn ? options.fn({k: key, v: context[key]}) : context[key]);
672
          }
673
        }
674
675
        return retList.join(', ');
676
      });
677
678
      Handlebars.registerHelper('instrumentIcon', function(instrumentCode) {
679
        var icon = '<span class="instrumentTextIcon">' + instrumentCode + '</span>';
680
        if (that.displayOptions.hasOwnProperty('iconMapHtml')) {
681
          if (that.displayOptions.iconMapHtml.hasOwnProperty(instrumentCode)) {
682
            icon = that.displayOptions.iconMapHtml[instrumentCode];
683
          }
684
        }
685
        return new Handlebars.SafeString(icon);
686
      });
687
688
      Handlebars.registerHelper('durationToMS', function(duration) {
689
        var seconds = (duration % 60);
690
        if (seconds < 10) {
691
          seconds = '0' + seconds;
692
        }
693
        return Math.floor(duration / 60) + ':' + seconds;
694
      });
695
696
      Handlebars.registerHelper('gameList', function(song) {
697
        return song.platforms.join(', ');
698
      });
699
700
      Handlebars.registerHelper('ifContains', function(haystack, needle, options) {
701
        return (haystack.indexOf(needle) === -1) ? '' : options.fn(this);
702
      });
703
704
      this.manageTemplate = Handlebars.compile(
705
        '<div class="ticket well well-sm {{#if ticket.used}}used{{/if}}' +
706
        ' {{#each ticket.song.platforms }}platform{{ this }} {{/each}}' +
707
        ' {{#if ticket.band.K}}withKeys{{/if}}"' +
708
        ' data-ticket-id="{{ ticket.id }}">' +
709
        '        <div class="pull-right">' +
710
        (function() {
711
          var s = '';
712
          for (var i = 0; i < that.platforms.length; i++) {
713
            var p = that.platforms[i];
714
            s += '<div class="gameMarker gameMarker' + p + '">' +
715
              '{{#ifContains ticket.song.platforms "' + p + '" }}' + p + '{{/ifContains}}</div>';
716
          }
717
          return s;
718
        })() +
719
        '        <button class="btn btn-primary performButton" data-ticket-id="{{ ticket.id }}">Performing</button>' +
720
        '        <button class="btn btn-danger removeButton" data-ticket-id="{{ ticket.id }}">Remove</button>' +
721
        '        <button class="btn editButton" data-ticket-id="{{ ticket.id }}">' +
722
        '<span class="fa fa-edit" title="Edit"></span>' +
723
        '</button>' +
724
        '        </div>' +
725
        '<div class="ticketOrder">' +
726
        '<div class="ticketOrdinal"></div>' +
727
        '<div class="ticketTime"></div>' +
728
        '</div>' +
729
        '<div class="ticketId">' +
730
        '<span class="fa fa-ticket"></span> {{ ticket.id }}</div> ' +
731
        '<div class="ticketMeta">' +
732
        '<div class="blocking">' +
733
        '{{#if ticket.blocking}}<span class="fa fa-hand-stop-o" title="Blocking" />{{/if}}' +
734
        '</div>' +
735
        '<div class="private">' +
736
        '{{#if ticket.private}}<span class="fa fa-eye-slash" title="Private" />{{/if}}' +
737
        '</div>' +
738
        '</div>' +
739
        '<div class="pendingSong">' +
740
        '<span class="fa fa-group"></span> ' +
741
742
        // Display performers with metadata if valid, else just the band title.
743
        /*
744
         '{{#if ticket.performers}}' +
745
         '{{#each ticket.performers}}' +
746
         '<span class="performer performerDoneCount{{songsDone}}" ' +
747
         'data-performer-id="{{performerId}}"> {{performerName}} ' +
748
         ' (<span class="songsDone">{{songsDone}}</span>/<span class="songsTotal">{{songsTotal}}</span>)' +
749
         '</span>' +
750
         '{{/each}}' +
751
         '{{else}}' +
752
         '{{ ticket.title }}' +
753
         '{{/if}}' +
754
         */
755
756
        // Display performers with metadata if valid, else just the band title.
757
        '{{#if ticket.band}}' +
758
        '{{#each ticket.band}} <span class="instrumentTextIcon">{{ @key }}</span>' +
759
        '{{#each this}}' +
760
        '<span class="performer performerDoneCount{{songsDone}}" ' +
761
        'data-performer-id="{{performerId}}" data-performer-name="{{performerName}}"> {{performerName}} ' +
762
        ' (<span class="songsDone">{{songsDone}}</span>/<span class="songsTotal">{{songsTotal}}</span>)' +
763
        '</span>' +
764
        '{{/each}}' +
765
        '{{/each}}' +
766
        '{{/if}}' +
767
768
        '{{#if ticket.title}}' +
769
        '<span class="ticketTitleIcon"><span class="instrumentTextIcon">Title</span> {{ ticket.title }}</span>' +
770
        '{{/if}}' +
771
772
        '{{#if ticket.song}}<br /><span class="fa fa-music"></span> {{ticket.song.artist}}: ' +
773
        '{{ticket.song.title}}' +
774
        ' ({{gameList ticket.song}})' +
775
        '{{/if}}' +
776
        '</div>' +
777
        '</div>'
778
      );
779
780
      this.upcomingTicketTemplate = Handlebars.compile(
781
        '<div class="ticket well ' +
782
        (this.displayOptions.songInPreview ? 'withSong' : 'noSong') +
783
        ' ' +
784
        (this.displayOptions.title ? 'withTitle' : 'noTitle') + // TODO is this used (correctly)?
785
        '" data-ticket-id="{{ ticket.id }}">' +
786
787
        (this.displayOptions.adminQueueHasControls && this.displayOptions.isAdmin ?
788
          '<div class="ticketAdminControls">' +
789
          '<button class="btn btn-sm btn-primary performingButton"' +
790
          ' data-ticket-id="{{ ticket.id }}">Performing</button>' +
791
          '<button class="btn btn-sm btn-danger removeButton" data-ticket-id="{{ ticket.id }}">Remove</button>' +
792
          '</div>'
793
          : '') +
794
795
796
        '<div class="ticketMeta">' +
797
        '<div class="blocking">' +
798
        '{{#if ticket.blocking}}<span class="fa fa-hand-stop-o" title="Blocking" />{{/if}}' +
799
        '</div>' +
800
        '<div class="private">' +
801
        '{{#if ticket.private}}<span class="fa fa-eye-slash" title="Private" />{{/if}}' +
802
        '</div>' +
803
        '</div>' +
804
805
        '  <div class="ticket-inner">' +
806
        '    <p class="text-center band auto-font">{{ticket.title}}</p>' +
807
        '    <p class="performers auto-font" data-fixed-assetwidth="200">' +
808
        '{{#each ticket.band}}' +
809
        '<span class="instrumentTag">{{instrumentIcon @key}}</span>' +
810
        '<span class="instrumentPerformers">{{#commalist this}}{{v.performerName}}{{/commalist}}</span>' +
811
        '{{/each}}' +
812
        '    </p>' +
813
        (this.displayOptions.songInPreview ?
814
          '{{#if ticket.song}}<p class="text-center song auto-font">' +
815
          '{{ticket.song.artist}}: {{ticket.song.title}}' +
816
          ' ({{gameList ticket.song}})' +
817
          '</p>{{/if}}' : '') +
818
        '        </div>' +
819
        '</div>  '
820
      );
821
822
      this.songAutocompleteItemTemplate = Handlebars.compile(
823
        '<div class="acSong" data-song-id="{{ song.id }}">' +
824
        '        <div class="acSong-inner {{#if song.queued}}queued{{/if}}">' +
825
        '        {{song.artist}}: {{song.title}} ({{gameList song}}) ' +
826
        '        </div>' +
827
        '</div>  '
828
      );
829
830
      this.editTicketTemplate = Handlebars.compile(
831
        '<div class="editTicket well">' +
832
        '<div class="pull-right editTicketButtons">' +
833
        '<button class="blockingButton btn btn-warning toggleButton">' +
834
        '<span class="fa fa-hand-stop-o" /> Blocking ' +
835
        ' <input type="checkbox" class="blockingCheckbox" ' +
836
        '  {{#if ticket}}{{# if ticket.blocking }}checked="checked"{{/if}}{{/if}} /></button>' +
837
        '<button class="privacyButton btn btn-warning toggleButton">' +
838
        '<span class="fa fa-eye-slash" /> Private ' +
839
        ' <input type="checkbox" class="privateCheckbox" ' +
840
        '  {{#if ticket}}{{# if ticket.private }}checked="checked"{{/if}}{{/if}} /></button>' +
841
        '<button class="editTicketButton btn btn-success">' +
842
        '<span class="fa fa-save" /> Save</button>' +
843
        '<button class="cancelTicketButton btn">' +
844
        '<span class="fa fa-close" /> Cancel</button>' +
845
        '</div>' +
846
847
        '{{# if ticket}}' +
848
        '<h3 class="editTicketHeader">Edit ticket <span class="fa fa-ticket"></span> {{ticket.id}}</h3>' +
849
        '{{else}}<h3 class="newTicketHeader">Add new ticket</h3>{{/if}}' +
850
851
        '<div class="editTicketInner">' +
852
        '<div class="editTicketSong">' +
853
        '<div class="ticketAspectSummary"><span class="fa fa-music fa-2x" title="Song"></span> ' +
854
        '<input type="hidden" class="selectedSongId"/> ' +
855
        '<span class="selectedSong">{{#if ticket}}{{#if ticket.song}}{{ticket.song.artist}}: ' +
856
        '{{ticket.song.title}}{{/if}}{{/if}}</span>' +
857
858
        '<button title="Remove song from ticket" ' +
859
        'class="btn removeSongButton{{#unless ticket}}{{#unless ticket.song}} hidden{{/unless}}{{/unless}}">' +
860
        ' <span class="fa fa-ban" />' +
861
        '</button>' +
862
863
        '</div>' +
864
        '<div class="input-group input-group">' +
865
        '<span class="input-group-addon" id="search-addon1"><span class="fa fa-search"></span> </span>' +
866
        '<input class="addSongTitle form-control" placeholder="Search song or use code"/>' +
867
        '</div>' +
868
869
        '<div class="songCompleteOuter">' +
870
        '<div class="songComplete"></div>' +
871
        '</div>' + // /songCompleteOuter
872
        '</div>' + // /editTicketSong
873
874
        '<div class="editTicketBandColumn">' +
875
876
        '<div class="ticketAspectSummary"><span class="fa fa-group fa-2x pull-left" title="Performers"></span>' +
877
        '<span class="selectedBand">{{#if ticket}}{{ticket.title}}{{/if}}</span>' +
878
        '</div>' + // /ticketAspectSummary
879
880
        '<div class="input-group">' +
881
        '<span class="input-group-addon" id="group-addon-band"><span class="fa fa-pencil"></span> </span>' +
882
        '<input class="editTicketTitle form-control" placeholder="Band name or message (optional)"' +
883
        ' value="{{#if ticket}}{{ticket.title}}{{/if}}"/>' +
884
        '</div>' + // /input-group
885
886
        '<div class="bandControls">' +
887
        '<div class="bandTabsOuter">' +
888
        '<div class="instruments">' +
889
        ' <div class="instrument instrumentVocals instrumentSelected" data-instrument-shortcode="V">' +
890
        '  <div class="instrumentName">Vocals</div>' +
891
        '  <div class="instrumentPerformer"><i>Needed</i></div>' +
892
        ' </div>' +
893
        ' <div class="instrument instrumentGuitar" data-instrument-shortcode="G">' +
894
        '  <div class="instrumentName">Guitar</div>' +
895
        '  <div class="instrumentPerformer"><i>Needed</i></div>' +
896
        ' </div>' +
897
        ' <div class="instrument instrumentBass" data-instrument-shortcode="B">' +
898
        '  <div class="instrumentName">Bass</div>' +
899
        '  <div class="instrumentPerformer"><i>Needed</i></div>' +
900
        ' </div>' +
901
        ' <div class="instrument instrumentDrums" data-instrument-shortcode="D">' +
902
        '  <div class="instrumentName">Drums</div>' +
903
        '  <div class="instrumentPerformer"><i>Needed</i></div>' +
904
        ' </div>' +
905
        ' <div class="instrument instrumentKeys instrumentUnused" data-instrument-shortcode="K">' +
906
        '  <div class="instrumentName">Keyboard</div>' +
907
        '  <div class="instrumentPerformer"><i>Needed</i></div>' +
908
        ' </div>' +
909
        '</div>' + // /instruments
910
        '<div class="performerSelect">' +
911
        '<div class="input-group input-group">' +
912
        '<span class="input-group-addon" id="group-addon-performer"><span class="fa fa-plus"></span> </span>' +
913
        '<input class="newPerformer form-control" placeholder="New performer (Firstname Initial)"/>' +
914
        '</div>' +
915
916
        '<div class="performers"></div>' +
917
        '</div>' + // /performerSelect
918
        '</div>' + // /bandTabsOuter
919
        '</div>' + // /bandControls
920
        '</div>' + // /editTicketBandColumn
921
        '<div class="clearfix"></div>' + // Clear after editTicketBandColumn
922
        '</div>' + // /editTicketInner
923
        '</div>' // /editTicket
924
      );
925
926
      this.songDetailsTemplate = Handlebars.compile(
927
        '<div class="songDetails"><h3>{{song.artist}}: {{song.title}}</h3>' +
928
        '<table>' +
929
        '<tr><th>Duration</th><td>{{durationToMS song.duration}}</td></tr> ' +
930
        '<tr><th>Code</th><td>{{song.codeNumber}}</td></tr> ' +
931
        '<tr><th>Instruments </th><td>{{commalist song.instruments}}</td></tr> ' +
932
        '<tr><th>Games</th><td>{{commalist song.platforms}}</td></tr> ' +
933
        '<tr><th>Source</th><td>{{song.source}}</td></tr> ' +
934
        '</table>' +
935
        '</div>'
936
      );
937
938
    },
939
940
    ticketOrderChanged: function() {
941
      var that = this;
942
      var idOrder = [];
943
      $('#target').find('.ticket').each(
944
        function() {
945
          var ticketBlock = $(this);
946
          var ticketId = ticketBlock.data('ticketId');
947
          idOrder.push(ticketId);
948
        }
949
      );
950
951
      that.showAppMessage('Updating ticket order');
952
      $.ajax({
953
        method: 'POST',
954
        data: {
955
          idOrder: idOrder
956
        },
957
        url: '/api/newOrder',
958
        success: function(data, status) {
0 ignored issues
show
Unused Code introduced by
The parameter data is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
Unused Code introduced by
The parameter status is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
959
          // FIXME check return status
960
          that.showAppMessage('Saved revised order', 'success');
961
        },
962
        error: function(xhr, status, error) {
963
          var message = 'Failed to save revised order';
964
          that.reportAjaxError(message, xhr, status, error);
965
        }
966
      });
967
968
      this.updatePerformanceStats();
969
    },
970
971
    performButtonCallback: function(button) {
972
      var that = this;
973
974
      button = $(button);
975
      var ticketId = button.data('ticketId');
976
      that.showAppMessage('Mark ticket used');
977
      $.ajax({
978
          method: 'POST',
979
          data: {
980
            ticketId: ticketId
981
          },
982
          url: '/api/useTicket',
983
          success: function(data, status) {
984
            that.showAppMessage('Marked ticket used', 'success');
985
            void(data);
986
            void(status);
987
            var ticketBlock = $('.ticket[data-ticket-id="' + ticketId + '"]');
988
            ticketBlock.addClass('used');
989
            // TicketBlock.append(' (done)');
990
991
            // Fixme receive updated ticket info from API
992
            var ticket = ticketBlock.data('ticket');
993
            ticket.startTime = Date.now() / 1000;
994
            ticket.used = true;
995
            ticketBlock.data('ticket', ticket);
996
997
            that.updatePerformanceStats();
998
          },
999
          error: function(xhr, status, error) {
1000
            var message = 'Failed to mark ticket used';
1001
            that.reportAjaxError(message, xhr, status, error);
1002
          }
1003
        }
1004
      );
1005
    },
1006
1007
    removeButtonCallback: function(button) {
1008
      var that = this;
1009
      button = $(button);
1010
      var ticketId = button.data('ticketId');
1011
      that.showAppMessage('Deleting ticket');
1012
      $.ajax({
1013
          method: 'POST',
1014
          data: {
1015
            ticketId: ticketId
1016
          },
1017
          url: '/api/deleteTicket',
1018
          success: function(data, status) {
1019
            that.showAppMessage('Deleted ticket', 'success');
1020
            void(status);
1021
            var ticketBlock = $('.ticket[data-ticket-id="' + ticketId + '"]');
1022
            ticketBlock.remove();
1023
            that.updatePerformanceStats();
1024
          },
1025
          error: function(xhr, status, error) {
1026
            var message = 'Failed to deleted ticket';
1027
            that.reportAjaxError(message, xhr, status, error);
1028
          }
1029
        }
1030
      );
1031
    },
1032
1033
    editButtonCallback: function(button) {
1034
      var that = this;
1035
      button = $(button);
1036
      var ticketId = button.data('ticketId');
1037
1038
      var ticketBlock = $('.ticket[data-ticket-id="' + ticketId + '"]');
1039
      var ticket = ticketBlock.data('ticket'); // TODO possibly load from ajax instead?
1040
      that.resetEditTicketBlock(ticket);
1041
    },
1042
1043
    enableAcSongSelector: function(outerElement, songClickHandler) {
1044
      var that = this;
1045
      outerElement.find('.acSong').click(
1046
        function() {
1047
          // Find & decorate clicked element
1048
          outerElement.find('.acSong').removeClass('selected');
1049
          $(this).addClass('selected');
1050
1051
          var song = $(this).data('song');
1052
          songClickHandler.call(that, song); // Run in 'that' context
1053
        }
1054
      );
1055
    },
1056
1057
1058
    searchPageSongSelectionClick: function(song) {
1059
      var target = $('#searchTarget');
1060
      target.html(this.songDetailsTemplate({song: song}));
1061
    },
1062
1063
    updatePerformanceStats: function() {
1064
      var that = this;
1065
      var performed = {};
1066
      var lastByPerformer = {};
1067
      var ticketOrdinal = 1;
1068
      var ticketTime = null;
1069
1070
      var pad = function(number) {
1071
        if (number < 10) {
1072
          return '0' + number;
1073
        }
1074
        return number;
1075
      };
1076
1077
      // First check number of songs performed before this one
1078
      var sortContainer = $('.sortContainer');
1079
      var lastSongDuration = null;
1080
      var lastTicketNoSong = true;
1081
1082
      var nthUnused = 1;
1083
1084
      sortContainer.find('.ticket').each(function() {
1085
        var realTime;
1086
        var ticketId = $(this).data('ticket-id');
1087
        var ticketData = $(this).data('ticket');
1088
1089
        if (ticketData.startTime) {
1090
          realTime = new Date(ticketData.startTime * 1000);
1091
        }
1092
1093
        $(this).removeClass('shown');
1094
1095
        if (!(ticketData.used || ticketData.private)) {
1096
          if (nthUnused <= that.displayOptions.upcomingCount) {
1097
            $(this).addClass('shown');
1098
          }
1099
          nthUnused++;
1100
        }
1101
1102
        $(this).find('.ticketOrdinal').text('# ' + ticketOrdinal);
1103
        // Fixme read ticketStart from data if present
1104
        if (realTime) {
1105
          ticketTime = realTime;
1106
        } else if (ticketTime) {
1107
          // If last song had an implicit time, add defaultSongOffsetMs to it and assume next song starts then
1108
          // If this is in the past, assume it starts now!
1109
          var songOffsetMs;
1110
          if (lastTicketNoSong) {
1111
            songOffsetMs = that.defaultSongIntervalSeconds * 1000;
1112
            // Could just be a message, could be a reset / announcement, so treat as an interval only
1113
          } else if (lastSongDuration) {
1114
            songOffsetMs = (that.defaultSongIntervalSeconds + lastSongDuration) * 1000;
1115
          } else {
1116
            songOffsetMs = (that.defaultSongIntervalSeconds + that.defaultSongLengthSeconds) * 1000;
1117
          }
1118
          ticketTime = new Date(Math.max(ticketTime.getTime() + songOffsetMs, Date.now()));
1119
        } else {
1120
          ticketTime = new Date();
1121
        }
1122
        $(this).find('.ticketTime').text(pad(ticketTime.getHours()) + ':' + pad(ticketTime.getMinutes()));
1123
1124
        // Update performer stats (done/total)
1125
        $(this).find('.performer').each(function() {
1126
          var performerId = $(this).data('performer-id');
1127
          var performerName = $(this).data('performer-name');
1128
          if (!performed.hasOwnProperty(performerId)) {
1129
            performed[performerId] = 0;
1130
          }
1131
          $(this).find('.songsDone').text(performed[performerId]);
1132
1133
          $(this).removeClass(
1134
            function(i, oldClass) {
1135
              void(i);
1136
              var classes = oldClass.split(' ');
1137
              var toRemove = [];
1138
              for (var cIdx = 0; cIdx < classes.length; cIdx++) {
1139
                if (classes[cIdx].match(/^performerDoneCount/)) {
1140
                  toRemove.push(classes[cIdx]);
1141
                }
1142
              }
1143
              return toRemove.join(' ');
1144
            }
1145
          ).addClass('performerDoneCount' + performed[performerId]);
1146
          performed[performerId]++;
1147
1148
          // Now check proximity of last song by this performer
1149
          if (lastByPerformer.hasOwnProperty(performerId)) {
1150
            var distance = ticketOrdinal - lastByPerformer[performerId].idx;
1151
            $(this).removeClass('proximityIssue');
1152
            $(this).removeClass('proximityIssue1');
1153
            if ((distance < 3) && (performerName.charAt(0) !== '?')) {
1154
              $(this).addClass('proximityIssue');
1155
              if (distance === 1) {
1156
                $(this).addClass('proximityIssue1');
1157
              }
1158
            }
1159
          } else {
1160
            // Make sure they've not got a proximity marker on a ticket that's been dragged to top
1161
            $(this).removeClass('proximityIssue');
1162
          }
1163
          lastByPerformer[performerId] = {idx: ticketOrdinal, ticketId: ticketId};
1164
        });
1165
        ticketOrdinal++;
1166
1167
        if (ticketData.song) {
1168
          lastSongDuration = ticketData.song.duration;
1169
          lastTicketNoSong = false;
1170
        } else {
1171
          lastSongDuration = 0;
1172
          lastTicketNoSong = true;
1173
        } // Set non-song ticket to minimum duration
1174
      });
1175
1176
      // Then update all totals
1177
      sortContainer.find('.performer').each(function() {
1178
        var performerId = $(this).data('performer-id');
1179
        var totalPerformed = performed[performerId];
1180
        $(this).find('.songsTotal').text(totalPerformed);
1181
      });
1182
    },
1183
1184
    /**
1185
     * Show a message in the defined appMessageTarget (f any)
1186
     *
1187
     * @param message {string} Message to show (replaces any other)
1188
     * @param [className='info'] {string} 'info','success','warning','danger'
0 ignored issues
show
Documentation Bug introduced by
The parameter className='info' does not exist. Did you maybe mean className instead?
Loading history...
1189
     */
1190
    showAppMessage: function(message, className) {
1191
      var that = this;
1192
      if (this.messageTimer) {
1193
        clearTimeout(this.messageTimer);
1194
      }
1195
1196
      this.messageTimer = setTimeout(function() {
1197
        that.appMessageTarget.html('');
1198
      }, 5000);
1199
1200
      if (!className) {
1201
        className = 'info';
1202
      }
1203
      if (this.appMessageTarget) {
1204
        var block = $('<div />').addClass('alert alert-' + className);
1205
        block.text(message);
1206
        this.appMessageTarget.html('').append(block);
1207
      }
1208
    },
1209
1210
    ucFirst: function(string) {
1211
      return string.charAt(0).toUpperCase() + string.slice(1);
1212
    },
1213
1214
    reportAjaxError: function(message, xhr, status, error) {
1215
      this.showAppMessage(
1216
        this.ucFirst(status) + ': ' + message + ': ' + error + ', ' + xhr.responseJSON.error,
1217
        'danger'
1218
      );
1219
    },
1220
1221
    checkRemoteRedirect: function() {
1222
      window.setInterval(function() {
1223
          $.get('/api/remotesRedirect', function(newPath) {
1224
            if (newPath && (newPath !== window.location.pathname)) {
1225
              window.location.pathname = newPath;
1226
            }
1227
          });
1228
        },
1229
        10000);
1230
    }
1231
  };
1232
}());